﻿#region License
/*
    Copyright (c) 2010, Paweł Hofman (CodeTitans)
    All Rights Reserved.

    Licensed under the Apache License version 2.0.
    For more information please visit:

    http://codetitans.codeplex.com/license
        or
    http://www.apache.org/licenses/


    For latest source code, documentation, samples
    and more information please visit:

    http://codetitans.codeplex.com/
*/
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Diagnostics;

namespace CodeTitans.Bayeux
{
    /// <summary>
    /// Class responsible for transmitting data for designated web servers over HTTP protocol.
    /// </summary>
    public class HttpConnection
    {
        public const string MethodPost = "POST";
        public const string MethodGet = "GET";

        private const string HeaderSentAt = "X-Sent";

        private string _url;
        private Encoding _defaultEncoding;
        private WebRequest _request;

        #region Events

        /// <summary>
        /// Event fired if data has been received for current request.
        /// </summary>
        public event EventHandler<HttpConnectionEventArgs> DataReceived;
        /// <summary>
        /// Event fired if data reception failed for current request.
        /// </summary>
        public event EventHandler<HttpConnectionEventArgs> DataReceiveFailed;

        #endregion

        /// <summary>
        /// Init constructor.
        /// </summary>
        public HttpConnection(string url, Encoding defaultEncoding)
        {
            _url = url;
            _defaultEncoding = defaultEncoding != null ? defaultEncoding : Encoding.UTF8;
            Timeout = 20 * 1000;
        }

        /// <summary>
        /// Init constructor.
        /// </summary>
        public HttpConnection(string url)
            : this(url, null)
        {
        }

        public void SendRequest(string data, string method, bool asynchronous)
        {
            SendRequest(null, data, method, asynchronous);
        }

        public void SendRequest(string relativeUrlPath, string data, string method, bool asynchronous)
        {
            if (_request != null)
                throw new InvalidOperationException("Another request is being processed. Call Cancel() method first.");
            if (method != MethodPost && method != MethodGet)
                throw new InvalidOperationException("Invalid method used. Try 'POST' or 'GET'.");

            //////////////////////
            // FILL HTTP request:
            string uri = string.IsNullOrEmpty(relativeUrlPath) ? _url : _url + relativeUrlPath;
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
            CredentialCache credentials = new CredentialCache();
            DateTime now = DateTime.Now;

            if (NetworkCredential != null)
                credentials.Add(new Uri(uri), "Basic", NetworkCredential);

            webRequest.KeepAlive = false;
#if !PocketPC
            webRequest.CachePolicy = new System.Net.Cache.HttpRequestCachePolicy(System.Net.Cache.HttpRequestCacheLevel.NoCacheNoStore);
            webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
#endif
            webRequest.Pipelined = false;
            webRequest.Method = method;
            webRequest.Accept = AcceptContentType;
            webRequest.ContentType = ContentType;
            webRequest.Credentials = credentials;
            webRequest.Headers.Add(HeaderSentAt, now.Ticks.ToString());
            webRequest.Timeout = Timeout;
            _request = webRequest;

            AppendAdditionalInfo(webRequest);

            //////////////////////
            // Format data to sent:
            if (data != null)
            {
                try
                {
                    using (Stream requestStream = webRequest.GetRequestStream())
                    {
                        WriteRequestData(_defaultEncoding.GetBytes(data), requestStream);
                    }
                }
                catch (WebException ex)
                {
                    if (DataReceiveFailed != null)
                        DataReceiveFailed(this, new HttpConnectionEventArgs(this, HttpStatusCode.RequestTimeout, ex.Message));

                    _request = null;
                    return;
                }
            }

            Debug.WriteLine(string.Format("<--- Sending request (length: {0} bytes, at: {1}): {2}", data != null ? webRequest.ContentLength : 0, now, uri));
            Debug.WriteLine(data != null ? data : string.Empty);

            //////////////////////
            // Send request:
            if (asynchronous)
            {
                webRequest.BeginGetResponse (new AsyncCallback(AsyncWebResponseCallback), webRequest);
            }
            else
            {
                HttpWebResponse response = null;

                try
                {
                    response = (HttpWebResponse)webRequest.GetResponse();
                }
                catch (WebException ex)
                {
                    response = (HttpWebResponse)ex.Response;
                }

                try
                {
                    ProcessResponse(webRequest, response);
                }
                finally
                {
                    if (response != null)
                        response.Close();
                }
            }
        }

        public void Cancel()
        {
            if (_request == null)
                throw new InvalidOperationException("No operation to cancel");

            Debug.WriteLine("Aborting HTTP request!");
            _request.Abort();
            _request = null;
        }

        public bool IsActive
        {
            get { return _request != null; }
        }

        public string ContentType
        {
            get;
            set;
        }

        public string AcceptContentType
        {
            get { return ContentType; }
        }

        public NetworkCredential NetworkCredential
        {
            get;
            protected set;
        }

        /// <summary>
        /// Gets or sets the timeout for next request (ms).
        /// </summary>
        public int Timeout
        {
            get; set;
        }

        private void AsyncWebResponseCallback(IAsyncResult asyncResult)
        {
            HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
            HttpWebResponse response = null;

            try
            {
                response = (HttpWebResponse)request.EndGetResponse(asyncResult);
            }
            catch (WebException ex)
            {
                response = (HttpWebResponse)ex.Response;
            }

            try
            {
                ProcessResponse(request, response);
            }
            finally
            {
                if (response != null)
                    response.Close();
            }
        }

        /// <summary>
        /// Add new extensions to HttpWebRequest before sending to the server.
        /// </summary>
        protected virtual void AppendAdditionalInfo(HttpWebRequest request)
        {
        }

        protected void ProcessResponse(HttpWebRequest request, HttpWebResponse response)
        {
            // release current request:
            _request = null;

            // and process the data:
            DateTime now = DateTime.Now;
            DateTime sentAt = new DateTime(long.Parse(request.Headers[HeaderSentAt]));
            long contentLength = response.ContentLength;

            if (contentLength < 0 && response.Headers["Content-Length"] != null)
            {
                try
                {
                    contentLength = long.Parse(response.Headers["Content-Length"]);
                }
                catch
                {
                }
            }

            Debug.WriteLine(string.Format("---> Received response (length: {0} bytes, at: {1}, waiting: {2:F2} sec) with status: {3} ({4}, {5})", contentLength, now, (now - sentAt).TotalSeconds, response.StatusCode, (int)response.StatusCode, response.StatusDescription));

            if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created && response.StatusCode != HttpStatusCode.Accepted)
            {
                if (DataReceiveFailed != null)
                    DataReceiveFailed(this, new HttpConnectionEventArgs(this, response.StatusCode, response.StatusDescription));

                return;
            }

            string data;

            try
            {
                using (Stream responseStream = response.GetResponseStream())
                {
                    data = ReadResponseData(responseStream, (string.IsNullOrEmpty(response.ContentEncoding) ? _defaultEncoding : Encoding.GetEncoding(response.ContentEncoding)));
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(string.Format("Response parsing error: {0}", ex.Message));

                if (DataReceiveFailed != null)
                    DataReceiveFailed(this, new HttpConnectionEventArgs(this, response.StatusCode, response.StatusDescription));

                return;
            }

            Debug.WriteLine(data != null ? data : string.Empty);

            if (DataReceived != null)
                DataReceived(this, new HttpConnectionEventArgs(this, response.StatusCode, response.StatusDescription, data));
        }

        #region Virtual Methods

        protected virtual void WriteRequestData(byte[] data, Stream output)
        {
            output.Write(data, 0, data.Length);
        }

        protected virtual string ReadResponseData(Stream input, Encoding encoding)
        {
            if (input == null || !input.CanRead)
                return null;

            using (StreamReader reader = new StreamReader(input, encoding))
            {
                return reader.ReadToEnd();
            }
        }

        #endregion
    }
}
